﻿//-----------------------------------------------------------------------
// <copyright file="SmartManipulator.Rotate.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Manipulator.Smart
{
    using TileTool.Common.Math;
    using TileTool.Editor.Tool.Scene;
    using TileTool.Editor.Transform;
    using TileTool.Render;

    /// <summary>
    /// Rotate implementation of the smart manipulator.
    /// </summary>
    public partial class SmartManipulator
    {
        #region Fields - Private

        /// <summary>
        /// Interpolation amount on width for center X pos.
        /// </summary>
        private float centerWidthInterpAmount = 0.5f;

        /// <summary>
        /// Interpolation amount on height for center Y pos.
        /// </summary>
        private float centerHeightInterpAmount = 0.5f;

        #endregion

        #region Methods - Public

        /// <summary>
        /// Gets the rotate manipulation amount for the manipulator.
        /// </summary>
        /// <returns>Returns a matrix which represents the rotation.</returns>
        public Matrix2D Rotate_GetAmount()
        {
            Vector2D startAxis = this.GetRotateStartUnitAxis();
            Vector2D startPerp = new Vector2D(-startAxis.y, startAxis.x);
            Vector2D currentAxis = this.GetRotateCurrentUnitAxis();
            float cosAngle = MathUtility.Clamp(startAxis.Dot(currentAxis), -1.0f, 1.0f);
            float sign = System.Math.Sign(startPerp.Dot(currentAxis));
            ManipulatorHandle rotateCenterHandle = this.GetHandle(SmartManipulatorHandlePosition.RotateCenter);
            float radians = (float)System.Math.Acos(cosAngle) * sign;
            Vector2D rotateCenter = this.DragStartMatrix * rotateCenterHandle.DragStartPos;
            ITransformState state = this.Owner.State;
            ISnapState snapState = state.SnapState;

            if (snapState.RotateSnap)
            {
                radians = MathUtility.SnapValue(radians, MathUtility.DegreesToRadians(snapState.RotateSnapDegrees));
            }

            return this.BuildTransformMatrix(radians, rotateCenter);
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Setup rotate handles.
        /// </summary>
        private void Rotate_SetupHandles()
        {
            SmartManipulatorHandle handle = new SmartManipulatorHandle();

            handle.Id = (int)SmartManipulatorHandlePosition.Rotate;
            handle.Size = new Size2D(this.settings.RotateCircleRadius);
            handle.IdleColor = this.settings.RotateCircleColor;
            handle.SelectedColor = this.settings.RotateCircleSelectedColor;

            this.AddManipulatorHandle(handle);

            handle = new SmartManipulatorHandle();

            handle.Id = (int)SmartManipulatorHandlePosition.RotateCenter;
            handle.TriggerManipulate = false;
            handle.Size = new Size2D(this.settings.RotateCenterCircleRadius);
            handle.IdleColor = this.settings.RotateCenterCircleColor;
            handle.SelectedColor = this.settings.RotateCenterCircleSelectedColor;

            this.AddManipulatorHandle(handle);
        }

        /// <summary>
        /// Called when the manipulator is reset.
        /// </summary>
        private void Rotate_Reset()
        {
            this.centerWidthInterpAmount = 0.5f;
            this.centerHeightInterpAmount = 0.5f;
        }

        /// <summary>
        /// Update the rotation handle positions.
        /// </summary>
        private void Rotate_UpdateHandlePositions()
        {
            Rect2D rect = new Rect2D(Vector2D.Zero, this.Size);
            Vector2D[] rectPoints = rect.GetPoints();
            Vector2D offset = Vector2D.AxisY * this.settings.RotateCircleOffset;

            this.PositionHandle(SmartManipulatorHandlePosition.Rotate, ((rectPoints[2] + rectPoints[3]) * 0.5f) + offset);

            Vector2D center = new Vector2D();

            center.x = MathUtility.Lerp(this.centerWidthInterpAmount, rectPoints[0].x, rectPoints[1].x);
            center.y = MathUtility.Lerp(this.centerHeightInterpAmount, rectPoints[1].y, rectPoints[2].y);

            this.PositionHandle(SmartManipulatorHandlePosition.RotateCenter, center);
        }

        /// <summary>
        /// Called when a drag begins.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="canvasPos">Canvas position position where the drag started.</param>
        private void Rotate_DragBegin(SceneToolInputData sceneInputData, Vector2D canvasPos)
        {
        }

        /// <summary>
        /// Called when a drag ends.
        /// </summary>
        private void Rotate_DragEnd()
        {
        }

        /// <summary>
        /// Called when a drag update happens on any handle.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="canvasPos">Canvas position position where the mouse is currently.</param>
        private void Rotate_DragUpdate(SceneToolInputData sceneInputData, Vector2D canvasPos)
        {
            if (DragHandle.Id != (int)SmartManipulatorHandlePosition.RotateCenter)
            {
                return;
            }

            Matrix2D invHandleMtx = this.Matrix.Invert();
            Matrix2D invHandleNoTransMtx = invHandleMtx.RotationOnly();
            Vector2D newPosLocalSpace = DragHandle.DragStartPos + (invHandleNoTransMtx * DragAmountWorld);
            Vector2D localPos = invHandleMtx * DragCurrentWorldPos;

            bool snapped = this.CenterDrag_SnapAgainstHandles(localPos, ref newPosLocalSpace);

            if (!snapped)
            {
                snapped = this.CenterDrag_SnapAgainstRectCenter(localPos, ref newPosLocalSpace);
            }

            if (!snapped)
            {
                snapped = this.CenterDrag_SnapToGrid(localPos, invHandleMtx, ref newPosLocalSpace);
            }
            
            float width = this.Size.Width;
            float height = this.Size.Height;
            if (width <= MathUtility.Epsilon || height <= MathUtility.Epsilon)
            {
                return;
            }

            this.centerWidthInterpAmount = newPosLocalSpace.x / width;
            this.centerHeightInterpAmount = newPosLocalSpace.y / height;

            DragHandle.Pos = newPosLocalSpace;
        }

        /// <summary>
        /// During a rotation center drag, snap against manipulator handles.
        /// </summary>
        /// <param name="localPos">Mouse position in local space.</param>
        /// <param name="newPosLocalSpace">New position for rotation center.</param>
        /// <returns>Returns true if we snapped.</returns>
        private bool CenterDrag_SnapAgainstHandles(Vector2D localPos, ref Vector2D newPosLocalSpace)
        {
            foreach (ManipulatorHandle handle in Handles)
            {
                switch ((SmartManipulatorHandlePosition)handle.Id)
                {
                    case SmartManipulatorHandlePosition.Rotate:
                    case SmartManipulatorHandlePosition.RotateCenter:
                    case SmartManipulatorHandlePosition.Translate:
                    case SmartManipulatorHandlePosition.SizeTranslateToggle:
                        continue;
                }

                // Kludgy way to force collision against all handles even if they
                // are not visible.
                bool wasVisible = handle.Visible;

                handle.Visible = true;

                Vector2D handleLocalPos = localPos - handle.Pos;
                bool hit = handle.TestPoint(handleLocalPos);
                handle.Visible = wasVisible;
                if (hit)
                {
                    newPosLocalSpace = handle.Pos;
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// During a rotation center drag, snap against the handle rect center.
        /// </summary>
        /// <param name="localPos">Mouse position in local space.</param>
        /// <param name="newPosLocalSpace">New position for rotation center.</param>
        /// <returns>Returns true if we snapped.</returns>
        private bool CenterDrag_SnapAgainstRectCenter(Vector2D localPos, ref Vector2D newPosLocalSpace)
        {
            Rect2D rect = new Rect2D(Vector2D.Zero, this.Size);
            Vector2D center = rect.Center;
            Rect2D testRect = new Rect2D();

            testRect.Size = new Size2D(this.settings.RotateCenterCircleRadius);
            testRect.Pos = new Vector2D(center.x - (testRect.Width * 0.5f), center.y - (testRect.Height * 0.5f));

            if (testRect.Contains(localPos))
            {
                newPosLocalSpace = center;
                return true;
            }

            return false;
        }

        /// <summary>
        /// During a rotation center drag, snap against the world grid.
        /// </summary>
        /// <param name="localPos">Mouse position in local space.</param>
        /// <param name="invHandleMtx">Inverse handle matrix.</param>
        /// <param name="newPosLocalSpace">New position for rotation center.</param>
        /// <returns>Returns true if we snapped.</returns>
        private bool CenterDrag_SnapToGrid(Vector2D localPos, Matrix2D invHandleMtx, ref Vector2D newPosLocalSpace)
        {
            ITransformState state = this.Owner.State;
            ISnapState snapState = state.SnapState;

            if (!snapState.SnapToGrid)
            {
                return false;
            }

            Vector2D worldPos = state.Grid.SnapVector(DragCurrentWorldPos);

            newPosLocalSpace = invHandleMtx * worldPos;

            return true;
        }

        /// <summary>
        /// When dragging, the start unit axis is the reference we use for calculating 
        /// a rotation amount.
        /// </summary>
        /// <returns>Returns the start axis as a unit vector.</returns>
        private Vector2D GetRotateStartUnitAxis()
        {
            ManipulatorHandle rotateCenterHandle = this.GetHandle(SmartManipulatorHandlePosition.RotateCenter);
            Vector2D matToDragStart = DragStartWorldPos - (this.DragStartMatrix * rotateCenterHandle.DragStartPos);

            return matToDragStart.SafeUnitOrVec(Vector2D.AxisX);
        }

        /// <summary>
        /// When dragging, the current unit axis is the current axis represented
        /// by the user's drag. It is used in conjunction with GetRotateStartUnitAxis() 
        /// to compute a rotation amount.
        /// </summary>
        /// <returns>Returns the current axis as a unit vector.</returns>
        private Vector2D GetRotateCurrentUnitAxis()
        {
            ManipulatorHandle rotateCenterHandle = this.GetHandle(SmartManipulatorHandlePosition.RotateCenter);
            Vector2D matToDragCurrent = DragCurrentWorldPos - (this.DragStartMatrix * rotateCenterHandle.DragStartPos);

            return matToDragCurrent.SafeUnitOrVec(Vector2D.AxisX);
        }

        /// <summary>
        /// Build a rotate transform matrix.
        /// </summary>
        /// <param name="rotateRads">Rotation radians.</param>
        /// <param name="rotateCenter">Rotation center point.</param>
        /// <returns>Returns a rotation matrix.</returns>
        private Matrix2D BuildTransformMatrix(float rotateRads, Vector2D rotateCenter)
        {
            Matrix2D translateToManipulator = new Matrix2D();
            Matrix2D rotate = new Matrix2D();
            Matrix2D translateFromManipulator = new Matrix2D();

            translateToManipulator.BuildTranslate(-rotateCenter);
            rotate.BuildRotation(rotateRads);
            translateFromManipulator.BuildTranslate(rotateCenter);

            return translateFromManipulator * rotate * translateToManipulator;
        }

        #endregion
    }
}